package com.wutong.erb.filter;

import com.wutong.erb.entity.user.User;
import com.wutong.erb.mapper.UserMapper;
import com.wutong.erb.util.CookieUtil;
import com.wutong.erb.util.DeviceUtil;
import com.wutong.erb.util.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 事务相关的JWT认证过滤器
 * 流程，先验证长效Token是否失效（存在于Cookie中），如果失效，则直接返回401
 * 再验证短效Token是否失效（存在于Header中），如果失效，则直接返回401
 * 再验证用户是否存在
 * 最后认证通过，放行
 * <pre>{@code
 * String accessToken = JwtUtil.generateToken(
 *                 user.getUsername(),
 *                 Map.of(
 *                         "uid", user.getId(),
 *                         "userType", user.getUserType(),
 *                         "device", deviceHash,
 *                         "loginTime", System.currentTimeMillis()
 *                 ),
 *                 30, TimeUnit.MINUTES
 *         );
 * String refreshToken = JwtUtil.generateToken(
 *         user.getUsername(),
 *         Map.of(
 *                 "refresh", true,
 *                 "device", deviceHash,
 *                 "fingerprint", fingerPrintHash
 *         ),
 *         7, TimeUnit.DAYS
 * );}</pre>
 * @author wutong
 */
@Slf4j
@RequiredArgsConstructor
public class BusinessJwtAuthenticationFilter extends OncePerRequestFilter {

    private final UserMapper userMapper;
    private final RedisTemplate<String, String> redisTemplate;

    /**
     * 登录成功后，所有用户操作需要携带JWT Token，通过Token获取用户信息，并设置到SecurityContextHolder中
     * <pre>{@code
     * String accessToken = JwtUtil.generateToken(
     *         user.getUsername(),
     *         Map.of(
     *                 "userId", user.getId(),
     *                 "userType", user.getUserType()
     *         ), 12, TimeUnit.HOURS
     * );
     * String refreshToken = JwtUtil.generateToken(
     *         DeviceUtil.generateDeviceHash(request),
     *         Map.of(
     *                 "accessToken", accessToken,
     *                 "loginTime", System.currentTimeMillis()
     *         ), 7, TimeUnit.DAYS
     * );}</pre>
     * @param request 请求对象
     * @param response 响应对象
     * @param filterChain 过滤链
     */
    @Override
    protected void doFilterInternal(@NotNull HttpServletRequest request,
                                    @NotNull HttpServletResponse response,
                                    @NotNull FilterChain filterChain) throws IOException, ServletException {
        // 从Cookie中获取refreshToken，先验证是否存在
        Cookie refreshTokenCookie = CookieUtil.getCookie(request, "refreshToken");
        if (refreshTokenCookie == null) {
            log.warn("Cookie中刷新Token不存在，请重新登录");
            response.sendError(401, "请重新登录");
            return;
        }
        final long[] userId = new long[1];
        final long[] leftTime = new long[1];
        String refreshToken = redisTemplate.opsForValue().get("refreshToken-" + DeviceUtil.generateDeviceHash(request));
        if (refreshToken == null) {
            log.warn("Redis中刷新Token不存在，请重新登录");
            response.sendError(401, "请重新登录");
            return;
        }
        String accessToken = JwtUtil.getTokenClaims(
                refreshTokenCookie.getValue(), new JwtUtil.TokenClaimsExceptionHandler<>() {
                    @Override
                    public String normalHandler(Claims claims) throws IOException {
                        // 验证长效Token的设备信息是否一致
                        String deviceHash = claims.getSubject();
                        if (!DeviceUtil.isRequestFromValidDevice(request, deviceHash)) {
                            log.warn("刷新Token设备信息不匹配，请重新登录");
                            response.sendError(401, "请重新登录");
                            return "";
                        }
                        // 获取短效Token
                        String shortToken = claims.get("accessToken", String.class);
                        if (shortToken == null) {
                            log.warn("短效Token不存在，请重新登录");
                            response.sendError(401, "请重新登录");
                            return "";
                        }
                        // 验证短效Token是否失效
                        if (!JwtUtil.validateToken(shortToken)) {
                            log.warn("短效Token已失效，请重新登录");
                            return "";
                        }
                        userId[0] = claims.get("userId", Long.class);
                        leftTime[0] = claims.getExpiration().getTime() - System.currentTimeMillis();
                        return shortToken;
                    }

                    @Override
                    public String handleExpiredJwtException() throws IOException {
                        log.warn("刷新Token已失效，请重新登录");
                        response.sendError(401, "请重新登录");
                        return "";
                    }

                    @Override
                    public String handleIllegalArgumentException() throws Exception {
                        log.warn("刷新Token解析失败，请重新登录");
                        response.sendError(401, "请重新登录");
                        return "";
                    }

                    @Override
                    public String handleSignatureException() throws Exception {
                        log.warn("刷新Token签名错误，请重新登录");
                        response.sendError(401, "请重新登录");
                        return "";
                    }

                    @Override
                    public String handleMalformedJwtException() throws Exception {
                        log.warn("刷新Token格式错误，请重新登录");
                        response.sendError(401, "请重新登录");
                        return "";
                    }

                    @Override
                    public String handleOtherException(Exception e) throws Exception {
                        log.error("刷新Token验证失败", e);
                        response.sendError(500, "服务器内部错误");
                        return "";
                    }
                }
        );
        if (accessToken.isEmpty()) {
            return;
        }
        // 验证短时Token
        if (!JwtUtil.validateToken(accessToken)) {
            log.warn("短时Token已失效，将刷新短时Token");
            String newAccessToken = refreshToken(userMapper.selectById(userId[0]));
            String newRefreshToken = JwtUtil.generateToken(
                    DeviceUtil.generateDeviceHash(request),
                    Map.of(
                            "accessToken", newAccessToken,
                            "loginTime", System.currentTimeMillis()
                    ), (int) leftTime[0], TimeUnit.MILLISECONDS
            );
            log.debug("设置新的刷新Token，并添加到Cookie中");
            response.addCookie(
                    CookieUtil.createCookie("refreshToken", newRefreshToken)
                            .setMaxAge((int) leftTime[0] / 1000)
                            .setHttpOnly(true)
                            .setPath("/page/user/")
                            // TODO 生产环境需要设置https
                            .setSecure(false)
                            .build()
            );
            filterChain.doFilter(request, response);
        }
        // 未失效，验证用户是否存在
        User user = userMapper.selectById(userId[0]);
        if (user == null) {
            log.warn("用户不存在，请重新登录");
            response.sendError(401, "请重新登录");
            return;
        }
        // 认证通过，设置SecurityContextHolder
        List<GrantedAuthority> authorities = Stream.of(user.getUserType().toString())
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                user.getUsername(), null, authorities
        );
//        authentication.setDetails(UserDetail.builder()
//                .id(user.getId())
//                .username(user.getUsername())
//                .email(user.getEmail())
//                .userType(user.getUserType())
//                .build());
        SecurityContextHolder.getContext().setAuthentication(authentication);
        // 认证通过，放行并将短时Token添加到Cookie中，方便http请求中使用
        redisTemplate.opsForValue().set("accessToken-" + DeviceUtil.generateDeviceHash(request), accessToken);
        filterChain.doFilter(request, response);
    }

    private String refreshToken(User user) {
        return JwtUtil.generateToken(
                user.getUsername(),
                Map.of(
                        "userId", user.getId(),
                        "userType", user.getUserType()
                ), 12, TimeUnit.HOURS
        );
    }
}
